from datetime import datetime, timedelta
from typing import Any, Dict, Optional, Tuple
from uuid import uuid4

from jose import JWTError, jwt, ExpiredSignatureError
from passlib.context import CryptContext
from fastapi import HTTPException, status

from app.utils.timezone import now
from app.core.config import settings

pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")

TOKEN_TYPE_ACCESS = "access"
TOKEN_TYPE_REFRESH = "refresh"
TOKEN_TYPE_RESET = "reset"


class SecurityError(Exception):
    """Base security exception"""

    pass


class TokenError(SecurityError):
    """Token related errors"""

    pass


class TokenExpiredError(TokenError):
    """Token has expired"""

    pass


def get_password_hash(password: str) -> str:
    """Generate password hash using bcrypt"""
    return pwd_context.hash(password)


def verify_password(plain_password: str, hashed_password: str) -> bool:
    """Verify password against hash using bcrypt"""
    return pwd_context.verify(plain_password, hashed_password)


def password_meets_requirements(password: str) -> Tuple[bool, str]:
    """
    Check if password meets security requirements
    Returns (is_valid, error_message)
    """
    if len(password) < 8:
        return False, "Password must be at least 8 characters long"
    if not any(c.isupper() for c in password):
        return False, "Password must contain at least one uppercase letter"
    if not any(c.islower() for c in password):
        return False, "Password must contain at least one lowercase letter"
    if not any(c.isdigit() for c in password):
        return False, "Password must contain at least one number"
    if not any(c in "!@#$%^&*()_+-=[]{}|;:,.<>?" for c in password):
        return False, "Password must contain at least one special character"
    return True, ""


def _create_token(data: Dict[str, Any], token_type: str, expires_delta: Optional[timedelta] = None) -> str:
    """Internal function to create tokens with common logic"""
    to_encode = data.copy()

    # Add token type and jti (JWT ID) for security
    to_encode.update({"type": token_type, "jti": str(uuid4()), "iat": now()})

    # Set expiration
    if expires_delta:
        expire = now() + expires_delta
    else:
        if token_type == TOKEN_TYPE_ACCESS:
            expire = now() + timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
        elif token_type == TOKEN_TYPE_REFRESH:
            expire = now() + timedelta(minutes=settings.REFRESH_TOKEN_EXPIRE_MINUTES)
        else:  # reset token
            expire = now() + timedelta(minutes=settings.PASSWORD_RESET_TOKEN_EXPIRE_MINUTES)

    to_encode.update({"exp": expire})

    return jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM)


def create_access_token(data: Dict[str, Any], expires_delta: Optional[timedelta] = None) -> str:
    """Create access token"""
    return _create_token(data, TOKEN_TYPE_ACCESS, expires_delta)


def create_refresh_token(data: Dict[str, Any], expires_delta: Optional[timedelta] = None) -> str:
    """Create refresh token"""
    return _create_token(data, TOKEN_TYPE_REFRESH, expires_delta)


def create_password_reset_token(data: Dict[str, Any]) -> str:
    """Create password reset token with 30 minutes expiry"""
    return _create_token(data, TOKEN_TYPE_RESET)


def _decode_token(token: str, expected_type: Optional[str] = None) -> Dict[str, Any]:
    """Internal function to decode and validate tokens"""
    try:
        payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM])

        # Verify token type if expected_type is provided
        if expected_type and payload.get("type") != expected_type:
            raise TokenError(f"Invalid token type. Expected {expected_type}")

        return payload

    except ExpiredSignatureError:
        raise TokenExpiredError("Token has expired")
    except JWTError as e:
        raise TokenError(f"Invalid token: {str(e)}")


def decode_access_token(token: str) -> Dict[str, Any]:
    """Decode and validate access token"""
    try:
        return _decode_token(token, TOKEN_TYPE_ACCESS)
    except (TokenError, TokenExpiredError) as e:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail=str(e),
            headers={"WWW-Authenticate": "Bearer"},
        )


def verify_refresh_token(token: str) -> Dict[str, Any]:
    """Verify refresh token and return payload"""
    try:
        return _decode_token(token, TOKEN_TYPE_REFRESH)
    except (TokenError, TokenExpiredError) as e:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail=str(e),
        )


def verify_reset_token(token: str) -> Dict[str, Any]:
    """Verify password reset token and return payload"""
    try:
        return _decode_token(token, TOKEN_TYPE_RESET)
    except (TokenError, TokenExpiredError) as e:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail=str(e),
        )


async def is_token_blacklisted(token: str) -> bool:
    """检查令牌是否在黑名单中"""
    from app.core.redis_conn import redis_helper

    return bool(await redis_helper.redis.get(f"token_blacklist:{token}"))


async def add_token_to_blacklist(token: str, expire_seconds: int = None) -> bool:
    """将令牌添加到黑名单"""
    from app.core.redis_conn import redis_helper

    if not expire_seconds:
        try:
            # 解析令牌以获取过期时间
            payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM])
            exp = payload.get("exp")
            if exp:
                # 计算剩余过期时间
                now_timestamp = now().timestamp()
                expire_seconds = int(exp - now_timestamp)
                if expire_seconds <= 0:
                    return True  # 令牌已过期，无需加入黑名单
        except (JWTError, ValueError):
            expire_seconds = 60 * 60 * 24 * 1  # 默认1天

    return await redis_helper.redis.set(f"token_blacklist:{token}", "1", ex=expire_seconds)


async def remove_token_from_blacklist(token: str) -> bool:
    """从黑名单中移除令牌"""
    from app.core.redis_conn import redis_helper

    return bool(await redis_helper.redis.delete(f"token_blacklist:{token}"))


async def get_user_active_tokens(user_id: int) -> list[str]:
    """获取用户的所有活跃令牌"""
    from app.core.redis_conn import redis_helper

    pattern = f"user_tokens:{user_id}:*"
    keys = await redis_helper.redis.keys(pattern)
    if not keys:
        return []
    return [key.split(":")[-1] for key in keys]


async def add_user_token(user_id: int, token: str, expire_seconds: int = None) -> bool:
    """记录用户的令牌"""
    from app.core.redis_conn import redis_helper

    if not expire_seconds:
        try:
            payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM])
            exp = payload.get("exp")
            if exp:
                now_timestamp = now().timestamp()
                expire_seconds = int(exp - now_timestamp)
        except (JWTError, ValueError):
            expire_seconds = 60 * 60 * 24 * 1  # 默认1天

    return await redis_helper.redis.set(f"user_tokens:{user_id}:{token}", "1", ex=expire_seconds)


async def remove_user_token(user_id: int, token: str) -> bool:
    """移除用户的令牌记录"""
    from app.core.redis_conn import redis_helper

    return bool(await redis_helper.redis.delete(f"user_tokens:{user_id}:{token}"))


async def invalidate_all_user_tokens(user_id: int) -> bool:
    """使用户的所有令牌失效（踢出用户）"""
    tokens = await get_user_active_tokens(user_id)
    if not tokens:
        return True

    success = True
    for token in tokens:
        # 将令牌加入黑名单
        if not await add_token_to_blacklist(token):
            success = False
        # 移除用户令牌记录
        await remove_user_token(user_id, token)

    return success
